package com.dhms.vostok1.model

import android.annotation.SuppressLint
import android.app.Application
import android.bluetooth.BluetoothDevice
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.util.Base64
import android.util.Log
import androidx.lifecycle.*
import com.dhms.vostok1.MainApplication
import com.dhms.vostok1.data.BLEDevice
import com.dhms.vostok1.data.BLEDeviceType
import com.dhms.vostok1.data.VibAnalyseReqBody
import com.dhms.vostok1.data.spot_patrol.*
import com.dhms.vostok1.repository.CheckInstrumentRepository
import com.dhms.vostok1.repository.SpcTaskRepository
import com.dhms.vostok1.service.ble.BleService
import com.dhms.vostok1.service.ble.BleServiceCallback
import com.dhms.vostok1.service.ble.DHMSBleServiceImp
import com.dhms.vostok1.service.ble.ZhengChaoBleServiceImp
import com.dhms.vostok1.service.net.NetworkApi
import com.dhms.vostok1.utils.Utils
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.util.*
import kotlin.math.max

class CheckInstrumentViewModel(application: Application, checkInstrumentRepository: CheckInstrumentRepository) : AndroidViewModel(application), BleServiceCallback {

    private var zhengChaoBle = ZhengChaoBleServiceImp.getInstance(application, this)
    private var ble: BleService = zhengChaoBle

    val dhmsBle: BleService by lazy { DHMSBleServiceImp.getInstance(getApplication(), this) }

    private val _isScanning = MutableLiveData(false)
    val isScanning: LiveData<Boolean> = _isScanning

    private val _dirpcConnected = MutableLiveData(ConnectingState.DISCONNECTED)
    val dirpcConnected: LiveData<ConnectingState> = _dirpcConnected

    private var _bleDeviceList = MutableLiveData<List<BLEDevice>>(listOf())
    val bleDeviceList: LiveData<List<BLEDevice>> = _bleDeviceList

    private var _selectedBleDevice = MutableLiveData<BLEDevice?>(null)
    val selectedBleDevice: LiveData<BLEDevice?> = _selectedBleDevice

    private var onBeaconSearched: (() -> Unit)? = null

    private var beaconIdList: List<String>? = null

    private val searchedDeviceAddressList = mutableListOf<String>()

    private val _terminalInfoData = MutableLiveData<BLEDevice?>(null)
    val terminalInfoData: LiveData<BLEDevice?> = _terminalInfoData
    private val _powerData = MutableLiveData<CollectedData<Pair<Float, Float?>>?>(null)
    val powerData: LiveData<CollectedData<Pair<Float, Float?>>?> = _powerData
    private val _temperatureData = MutableLiveData<CollectedData<Float>?>(null)
    val temperatureData: LiveData<CollectedData<Float>?> = _temperatureData
    private val _collectedData = MutableLiveData<CollectedData<Map<String, Float>?>?>(null)
    val originCollectedData: LiveData<CollectedData<Map<String, Float>?>?> = _collectedData
    val collectedData: LiveData<Map<String, Float>?> = _collectedData.map {
        val tmpMap = mutableMapOf<String, Float>()
        it?.data?.entries?.forEach { item ->
            tmpMap[item.key] = "%.2f".format(item.value).toFloat()
        }
        return@map if (tmpMap.isEmpty()) null else tmpMap
    }
    private val _vibData = MutableLiveData<CollectedData<Map<String, Any>?>?>(null)
    val vibData: LiveData<CollectedData<Map<String, Any>?>?> = _vibData
    private val _collectionError = MutableLiveData(false)
    val collectionError: LiveData<Boolean?> = _collectionError

    private val _upgradingProgress = MutableLiveData(-1)
    val upgradingProgress: LiveData<Int> = _upgradingProgress
    private val _upgradingResult = MutableLiveData<Boolean?>(null)
    val upgradingResult: LiveData<Boolean?> = _upgradingResult

    private var _dataHasCollected = MutableLiveData<Boolean?>() // null 未采集，false 未采集完，true，采集完成
    val dataHasCollected: LiveData<Boolean?> = _dataHasCollected

    private var _notToDisconnectDevices = MutableLiveData(false)
    val notToDisconnectDevices: LiveData<Boolean> = _notToDisconnectDevices

//    private val _uploadTerminalInfoResult = MutableLiveData<ApiResult?>(null)
//    val uploadTerminalInfoResult: LiveData<ApiResult?> = _uploadTerminalInfoResult

    val checkInstrumentSpeedData: (model: String, rpm: Long) -> Flow<List<CheckInstrumentSpeedData>> =
        { model, rpm ->
            checkInstrumentRepository.checkInstrumentSpeedData(model, rpm)
        }

    private var _reportTerminalInfo = MutableLiveData<ReportTerminalInfo?>()
    val reportTerminalInfo: LiveData<ReportTerminalInfo?> = _reportTerminalInfo
    val powerObservable= MutableStateFlow(10f)
    var currentDevice:BLEDevice?=null

    private val bleScanCallback = object : ScanCallback() {
        @SuppressLint("MissingPermission")
        override fun onScanResult(callbackType: Int, result: ScanResult?) {
            super.onScanResult(callbackType, result)
            val macAddress = result?.device?.address ?: return
            result.let {
                val manufacturerData = it.scanRecord?.getManufacturerSpecificData(0X004c)
                // for Beijing zhengchao's spc device
                if (it.device.name?.subSequence(0, 2)?.equals("ZC") == true) {
                    Log.d(BLE_SEARCH_RESULT, "Beijing zhengchao's spc device found")
                    onSpcDeviceFound(it.device, BLEDeviceType.ZHENG_CHAO_SPC_DEVICE, null)
                } else if (it.device.name?.startsWith("Mbeacon") == true) {
                    // for Shanghai shanyuan's beacon device
                    // device model: KGE2.4
                    Log.d(BLE_SEARCH_RESULT, "Shanghai shanyuan's beacon found")
                    onBeaconFound(Utils.parseProductId(macAddress))
                } else if (manufacturerData != null && manufacturerData.size >= 23) {
                    // for DHMS beacon
                    // ascii DMS -> 444D5300
                    val dhmsBroadcastData = manufacturerData.asList()
                    val dhmsDeviceId = String(dhmsBroadcastData.subList(2, 5).toByteArray())
                    val deviceType =
                        dhmsBroadcastData[6].toInt()
                    val swVersion = dhmsBroadcastData.subList(8, 10).joinToString(".")
                    val hwVersion = dhmsBroadcastData.subList(10, 12).joinToString(".")
                    val productId = dhmsBroadcastData.subList(12, 18)
                        .joinToString("-") { b -> b.toString(16).padStart(2, '0').uppercase() }
                    val minorBytes = dhmsBroadcastData.subList(20, 22)
                    val power = ((minorBytes[0].toInt() shl 8) + minorBytes[1]) / 1000f

                    if (dhmsDeviceId != "DMS") {
                        return@let
                    }
                    Log.d(BLE_SEARCH_RESULT,
                        "get DHMS device. DHMS device: $dhmsDeviceId, deviceType: $deviceType," +
                                " swVersion: $swVersion, hwVersion: $hwVersion, productId: $productId" +
                                " power: $power, dhmsBroadcastData: ${
                                    dhmsBroadcastData.joinToString(
                                        "-"
                                    ) { b -> b.toString(16).padStart(2, '0').uppercase() }
                                }"
                    )

                    onDHMSDeviceFound(
                        BLEDeviceType.from(deviceType),
                        swVersion,
                        hwVersion,
                        productId,
                        power,
                        it.device
                    )
                }
            }
        }
    }

    @Suppress("UNUSED_PARAMETER")
    private fun onDHMSDeviceFound(bleDeviceType: BLEDeviceType, swVersion: String, hwVersion: String, productId: String, power: Float, device: BluetoothDevice) {
        if (bleDeviceType == BLEDeviceType.DHMS_BEACON) {
            onBeaconFound(Utils.parseProductId(device.address))
        } else if (bleDeviceType == BLEDeviceType.DHMS_SPC_DEVICE) {
            onSpcDeviceFound(device, bleDeviceType, power)
        } else {
            Log.d(
                BLE_SEARCH_RESULT, "Unknown DHMS device. DeviceType: $bleDeviceType"
            )
        }
    }

    private fun onBeaconFound(productId: String) {
        if (beaconIdList.isNullOrEmpty()) return
        if (productId in beaconIdList!!
        ) {
            Log.d(BLE_SEARCH_RESULT, "beacon found: $productId")
            onBeaconSearched?.let { func -> func() }
            onBeaconSearched = null
            beaconIdList = null
            return
        }
    }

    /**
     * Start devices discovery
     */
    fun startDeviceDiscovery() {
        Log.d(TAG, "Device discovery started")
        _bleDeviceList.value = mutableListOf()
        searchedDeviceAddressList.clear()
        if (dirpcConnected.value == ConnectingState.CONNECTED && selectedBleDevice.value != null) {
            _bleDeviceList.value = mutableListOf(selectedBleDevice.value!!)
            searchedDeviceAddressList.add(selectedBleDevice.value!!.macAddress)
        }
        _isScanning.value = true
        ble.scan(null, bleScanCallback)
        viewModelScope.launch {
            delay(DHMSBleServiceImp.SCAN_DURATION_SECOND * 1000)
            _isScanning.value = false
        }
    }

    /**
     * Stop devices discovery
     */
    fun stopDeviceDiscovery() {
        Log.d(TAG, "Device discovery stopped")
        _isScanning.value = false
        ble.stopScan(bleScanCallback)
    }

    fun startBeaconDiscovery(
        beaconIdList: List<String>,
        onBeaconSearched: () -> Unit
    ) {
        Log.d(TAG, "Beacon discovery started")
        this.beaconIdList = beaconIdList
        this.onBeaconSearched = onBeaconSearched

        ble.scan(null, bleScanCallback)
    }

    fun stopBeaconDiscovery() {
        Log.d(TAG, "Beacon discovery stopped")
        ble.stopScan(bleScanCallback)
        beaconIdList = null
    }

    fun selectBLEDevice(bleDevice: BLEDevice) {
        _selectedBleDevice.value = bleDevice
        bleDevice.power?.let {
            _powerData.value = CollectedData(
                bleDevice.productId,
                (Date().time / 1000).toInt(),
                Pair(Utils.dhmsSpcDeviceBatteryLevel(it), it)
            )
        }
        _dirpcConnected.value = ConnectingState.CONNECTING
        stopDeviceDiscovery()

        selectedBleDevice.value?.let {
            if (it.type == BLEDeviceType.DHMS_SPC_DEVICE) {
                ble = dhmsBle
            } else if (it.type == BLEDeviceType.ZHENG_CHAO_SPC_DEVICE) {
                ble = zhengChaoBle
            } else {
                // not supported device
            }
            ble.connectDevice(it.device)
        }
    }

    fun upgradeFirmware(
        device: BluetoothDevice,
        firmwareFile: File,
        versionNum: Int,
    ) {
        try {
            _upgradingProgress.value = -1
            _upgradingResult.value = null
            ble.upgrade(device, firmwareFile.name, versionNum, firmwareFile.readBytes())
        } catch (ex: Exception) {
            Log.e(TAG, "failed to read firmware file: $firmwareFile.name", ex)
        }
    }

    fun disconnectDevice() {
        Log.d(TAG, "Try to disconnect the connected device.")
        ble.closeConnect()
    }

    fun collectTerminalInfo(device: BluetoothDevice) {
        _collectionError.value = false
        _terminalInfoData.value = null
        currentDevice=null
        ble.requestDeviceInfo(device)
    }

    fun collectPowerLevel() {
        _collectionError.value = false
        ble.requestPowerLevel()
    }

    fun collectVibData(sample: Long, sampleRate: Long, speed: Long) {
        _collectionError.value = false
        ble.requestVibData(sample, sampleRate, speed)
    }

    fun collectTemperatureData() {
        _collectionError.value = false
        ble.requestTemperature()
    }

    fun collectVibAnalysisData() {
        _collectionError.value = false
        ble.requestVibAnalysis()
    }

    fun setDataHasCollected(dataHasCollected: Boolean?) {
        _dataHasCollected.value = dataHasCollected
    }

    fun setNotToDisconnectDevices(notToDisconnectDevices: Boolean) {
        _notToDisconnectDevices.value = notToDisconnectDevices
    }

    fun clearCollectedData() {
        _collectedData.value = null
        _powerData.value = null
        _temperatureData.value = null
        _vibData.value = null
        _collectionError.value = null
        _dataHasCollected.value = null
    }

    fun uploadTerminalInfo(isOnline: Boolean, info: ReportTerminalInfo) {
        viewModelScope.launch {
            val res = SpcTaskRepository.getInstance().uploadTerminalInfo(isOnline, info)
            Log.d(TAG, "res: $res")
            val e = res.exceptionOrNull()
            if (e != null) {
                Log.e(TAG, "Upload terminal info data error: $e")
                return@launch
            }
        }
    }

    suspend fun uploadTerminalTSData(tsData: ReportTsData): Throwable? {
        val res =
            NetworkApi.uploadTerminalTSData(tsData)
        Log.d(TAG, "res: $res")
        val e = res.exceptionOrNull()

        if (e != null) {
            Log.e(TAG, "Upload terminal info data error: $e")
            return e
        }
        return null
    }

    fun getUploadTerminalTSData(
        cpId: Long?,
        terminalId: Int
    ): ReportTsData {
        val time = max(
            max(
                powerData.value?.time ?: 0,
                temperatureData.value?.time ?: 0
            ), originCollectedData.value?.time ?: 0
        )
        val tsData =
            if (originCollectedData.value != null) LinkedHashMap(
                originCollectedData.value?.data
            ) else LinkedHashMap()
        tsData["BVLT"] = powerData.value?.data?.second ?: 0f
        tsData["TMPR"] = temperatureData.value?.data ?: 0f
        if (this.vibData.value != null) {
            tsData["VIBF"] = 1F
        }
        return ReportTsData(
            if (time != 0) time else (Date().time / 1000).toInt(),
            "{\"collection_point_id\":$cpId,\"type\":\"spot\"}",
            arrayListOf(tsData),
            terminalSn = terminalId
        )
    }

    suspend fun uploadTerminalBlockData(blockData: ReportBlockData): Throwable? {
        val res =
            NetworkApi.uploadTerminalBlockData(blockData)
        Log.d(TAG, "res: $res")
        val e = res.exceptionOrNull()
        if (e != null) {
            Log.e(TAG, "Upload terminal info data error: $e")
            return e
        }
        return null
    }

    suspend fun uploadTerminalInfo(terminalInfo: TerminalInfo) {

    }

    fun getReportBlockData(
        cpId: Long?,
        groupID: String?,
        taskId: Long?,
        terminalId: Int?
    ): ReportBlockData {
        val vibFile =
            (_vibData.value?.data?.get("FILE_DATA")?:ByteArray(0)) as ByteArray?
        val vibB64 = Base64.encodeToString(vibFile, Base64.DEFAULT)
        val tsDataTime = max(
            max(
                powerData.value?.time ?: 0,
                temperatureData.value?.time ?: 0
            ), originCollectedData.value?.time ?: 0
        )
        val time = if (tsDataTime == 0) {
            _vibData.value?.time
                ?: (Date().time / 1000).toInt()
        } else {
            tsDataTime
        }
        return ReportBlockData(
            time,
            "{\"collection_point_id\":$cpId,\"type\":\"spot\",\"group_task_id\":\"${groupID}-${taskId}\"}",
            _vibData.value?.data?.get("FILE_TYPE")
                ?.toString()
                ?.uppercase() ?: "ZIPF",
            vibB64,
            terminalSn = terminalId
        )
    }

    @Suppress("UNCHECKED_CAST")
    fun getAnalysedData(filePath: String) {
        viewModelScope.launch {
            try {
                val req = VibAnalyseReqBody(files = listOf(filePath))
                val res = NetworkApi.vibAnalyse(req)
                val chartDataItem = (res.body() as List<Map<String, Any>>).find {
                    it["data"] != null
                }?.get("data") as ArrayList<Double>
                _vibData.value = CollectedData(
                    data = mapOf("VACC" to chartDataItem)
                )
            } catch (e: Exception) {
                Log.e(TAG, "Getting analysed data failed.", e)
            }
        }
    }

    fun getAnalysedData(vibFileData: ArrayList<Double>?) {
        viewModelScope.launch {
            try {
                if (vibFileData == null) return@launch
                _vibData.value = CollectedData(
                    data = mapOf("VACC" to vibFileData)
                )
            } catch (e: Exception) {
                Log.e(TAG, "Getting analysed data failed.", e)
            }
        }
    }

    suspend fun getLatestFirmware(
        model: String,
        version: Int,
        filePath: String,
        onFileDownloaded: (file: File?) -> Unit
    ) {
        val res = NetworkApi.getFirmwares(
            FirmwaresReqQuery(
                1, 1,
                modelNO = model,
                sort = "version",
                publishStatus = 1,
                useStatus = 1
            )
        )
        Log.d(TAG, "The res to get latest firmware of model $model data is: $res.")
        val error = res.exceptionOrNull()
        if (error != null) {
            Log.e(TAG, "error: $error")
            return
        }
        val resData = res.getOrNull()?.body()?.data?.items
        if (resData.isNullOrEmpty()) {
            return
        }
        if (version >= resData[0].versionNum) {
            return
        }
        val fileUrl = resData[0].firmware?.get("url") ?: return
//        val fileUrl =
//            "http://pvzdev-dhmsiot.dhms.net/oss/assets/file/2022/03/6be9a79_DMS1200I_FMWR_0.38.bin"

        Utils.downloadFile(fileUrl, filePath, onFileDownloaded)
    }

    // region implement BleService.BleServiceCallback
    override fun onConnected() {
        viewModelScope.launch {
            _dirpcConnected.value = ConnectingState.CONNECTED
        }
    }

    override fun onDisconnected() {
        viewModelScope.launch {
            _dirpcConnected.value = ConnectingState.DISCONNECTED
            _terminalInfoData.value = null
            currentDevice=null
        }
    }

    override fun onCollectError() {
        _collectionError.value = true
    }

    override fun onDeviceInfoDataReceived(deviceInfo: BLEDevice) {
        viewModelScope.launch {
            Log.d(TAG, "onDeviceInfoDataReceived: $deviceInfo")
            _terminalInfoData.value = deviceInfo
            currentDevice=deviceInfo
            // upload terminal info
            val terminalId = deviceInfo.productId.toInt(16)
            _reportTerminalInfo.value = ReportTerminalInfo(
                terminalSn = terminalId,
                timestamp = (Date().time / 1000).toInt(),
                extraInfo = "{\"type\":\"spot\"}",
                info = TerminalInfo(
                    terminalId,
                    model = deviceInfo.model,
                    macAddr = deviceInfo.device.address,
                    swVersions = mapOf(
                        Pair(
                            first = deviceInfo.softwareVersions?.keys?.first() ?: "FMWR",
                            second = deviceInfo.swFourccVersions?.get(0)
                        )
                    ),
                    hwVersion = deviceInfo.hwVersion,
                    hwVersionNum = deviceInfo.hwVersionsNum,
                    attribute = if (deviceInfo.attr?.isNaN() != true) deviceInfo.attr else null,
                    //                        numSwImage = deviceInfo.numSwImage,
                    swFourcc = deviceInfo.softwareVersions?.keys?.toList(),
                    swFourccVersions = deviceInfo.swFourccVersions?.asList(),
                ),
                feature = TerminalFeature(
                    deviceInfo.feature?.collects ?: listOf()
                )
            )
        }
    }

    override fun onSpcDeviceFound(
        device: BluetoothDevice?,
        deviceType: BLEDeviceType,
        power: Float?
    ) {
        device?.let { d ->
            if (d.address !in searchedDeviceAddressList) {
                _bleDeviceList.value =
                    listOf(
                        *(bleDeviceList.value?.map { it }?.toTypedArray() ?: arrayOf()),
                        BLEDevice(
                            d,
                            deviceType,
                            Utils.parseProductId(device.address),
                            "",
                            d.address,
                            null,
                            null,
                            null,
                            null,
                            power = power,
                            powerLevel = power?.let { Utils.dhmsSpcDeviceBatteryLevel(it) }
                        )
                    )
                searchedDeviceAddressList.add(d.address)
            }
        }
    }

    override fun onTemperatureDataReceived(temperature: Float) {
        viewModelScope.launch {
            Log.d(
                TAG, "get temperature: $temperature"
            )
            _temperatureData.value = CollectedData(
                terminalInfoData.value?.productId ?: selectedBleDevice.value?.productId,
                (Date().time / 1000).toInt(),
                temperature
            )
            if (dataHasCollected.value != null) {
                _dataHasCollected.value = true
            }
        }
    }

    override fun onPowerLevelDataReceived(percent: Float, power: Float?) {
        viewModelScope.launch {
            Log.d(
                TAG, "get power info: ${percent}%, ${power}v"
            )
            _powerData.value = CollectedData(
                terminalInfoData.value?.productId ?: selectedBleDevice.value?.productId,
                (Date().time / 1000).toInt(),
                Pair(percent, power)
            )

            power?.let {
                powerObservable.value = it
            }
            _selectedBleDevice.value?.power = power

        }
    }

    override fun onVibAnalysisResultReceived(result: Map<String, Float>) {
        viewModelScope.launch {
            Log.d(
                TAG, "get vib analysis result info: $result"
            )
            _collectedData.value = CollectedData(
                terminalInfoData.value?.productId ?: selectedBleDevice.value?.productId,
                (Date().time / 1000).toInt(),
                result
            )
            if (dataHasCollected.value != null) {
                _dataHasCollected.value = true
            }
        }
    }

    override fun onVibDataReceived(vibData: HashMap<String, Any>) {
        viewModelScope.launch {
            Log.d(
                TAG, "get vib data:"
            )
            vibData["FILE_TYPE"]?.let {
                val fileType = if ((it as String) == "ZIPF") "txt.gzip" else "aiff"
                val gzipContent = vibData["FILE_DATA"] as ByteArray
                val vibFile = File(
                    getApplication<MainApplication>().getExternalFilesDir(null),
                    "${Date().time}.${fileType}"
                ).path
                vibData["FILE_PATH"] = vibFile
                saveAiffInLocal(vibFile, gzipContent)
                _vibData.value = CollectedData(
                    terminalInfoData.value?.productId ?: selectedBleDevice.value?.productId,
                    (Date().time / 1000).toInt(),
                    vibData
                )
                if (dataHasCollected.value != null) {
                    _dataHasCollected.value = true
                }
            }
        }
    }

    override fun onUpgradeResult(progress: Float, succeed: Boolean?) {
        Log.d(
            TAG, "progress: $progress: succeed:$succeed"
        )
        viewModelScope.launch {
            _upgradingProgress.value = (progress * 100).toInt()
            _upgradingResult.value = succeed
        }
    }

    private fun saveAiffInLocal(filePath: String, gzipContent: ByteArray) {
        Thread {
            var fileOutputStream: FileOutputStream? = null
            try {
                val vibFile = File(filePath)
                fileOutputStream = FileOutputStream(vibFile)
                fileOutputStream.write(gzipContent)

                Log.d(
                    TAG, "save block data in : ${vibFile.path}"
                )
            } catch (err: Exception) {
                Log.e(
                    TAG, err.toString()
                )
            } finally {
                fileOutputStream?.let {
                    it.close()
                    fileOutputStream = null
                }
            }
        }.start()
    }
    // endregion implement BleService.BleServiceCallback

    companion object {
        private const val TAG = "CheckInstrumentViewModel"
        private const val BLE_SEARCH_RESULT = "BLE_SEARCH_RESULT"

        enum class ConnectingState {
            DISCONNECTED, CONNECTING, CONNECTED
        }
    }
}

class CheckInstrumentViewModelFactory(
    private val application: Application,
    private val checkInstrumentRepository: CheckInstrumentRepository
) :
    ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(CheckInstrumentViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return CheckInstrumentViewModel(application, checkInstrumentRepository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}